﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Migrator.Entities;
using Migrator.Util;
using Migrator.Packets;
using Migrator.Services.Registration.Packets;
using Migrator.Core;
using Migrator.Services.RouteOptimization.Packets;

namespace Migrator.Services.MobileAgents
{
    /// <summary>
    /// Provides the home agent capability
    /// </summary>
    public class HomeAgent : IService
    {
        public HomeAgent(Node client)
        {
            this.Client = client;
            this.Client.BeforePacketSent += new Node.BeforePacketSentEventHandler(Client_BeforePacketSent);
        }

      
        public void Update()
        {
            this.bindingTable.RemoveExpired(Simulator.Instance.Clock);
        }

        public bool Filter(IPPacket packet)
        {
            // Home agents only care for registration requests
            RegistrationRequestPacket regPacket = packet as RegistrationRequestPacket;
            if (regPacket != null)
            {
                // Don't handle the request if I'm the foreign agent
                return this.Client.IPAddress == regPacket.HomeAgent;
            }

            return false;
        }

        public void Handle(IPPacket packet)
        {
            RegistrationRequestPacket regPacket = packet as RegistrationRequestPacket;
            if (regPacket != null)
            {
                this.Handle(regPacket);
            }
        }

        public IService Configure(params object[] configuration)
        {
            this.Enabled = true;
            return this;
        }

        public bool Enabled
        {
            get;
            set;
        }

        public Node Client
        {
            get;
            private set;
        }

        private void Handle(RegistrationRequestPacket packet)
        {
            // Reply to the request
            RegistrationReplyPacket reply = new RegistrationReplyPacket(this.Client, packet.SourceIP, packet.SourcePort);
            reply.HomeAddress = packet.HomeAddress;
            reply.HomeAgent = packet.HomeAgent;
            reply.Lifetime = Math.Min(packet.Lifetime, this.maxLifetime);

            // Successful by default
            reply.Code = (byte) (!supportSimultaneousBindings && packet.S ? 1 : 0);

            // If the IP address specified in the home agent field is wrong,
            // deny the request with error code = 136, and put the correct IP
            // address in the home agent field.
            if (packet.HomeAgent != this.Client.IPAddress)
            {
                reply.Code = 136;
                reply.HomeAgent = this.Client.IPAddress;
            }

            // If the registration has been successful
            if (reply.Code < 10)
            {
                // Deregister
                if (packet.Lifetime == 0)
                {
                    if (packet.CareOfAddress == packet.HomeAddress)
                    {
                        this.bindingTable.RemoveAll(packet.HomeAddress);
                    }
                    else
                    {
                        this.bindingTable.RemoveAll(packet.HomeAddress);
                       // this.bindingTable.Remove(packet.CareOfAddress);
                    }
                }
                else
                {
                    // Add a new association to the binding table
                    bool useSimultaneousBindings = packet.S && supportSimultaneousBindings;
                    var entry = new BindingTable.BindingEntry(packet.HomeAddress, packet.CareOfAddress, Simulator.Instance.Clock + packet.Lifetime);
                    this.bindingTable.Add(entry, !useSimultaneousBindings);
                }
            }

            // If the mobile node is deregistering, I must send the message 
            // to him through the link-layer, because he is not yet on this 
            // router's network and thus, Send() will fail.
            if (packet.Lifetime == 0)
            {
                this.Client.SendLinkLayer(reply, packet.Source.MACAddress);
            }
            else
            {
                this.Client.Send(reply);
            }
        }

        private IPPacket InitializeBindingUpdate(IPAddress homeAddress, IPAddress targetAddress)
        {
            var entries = this.bindingTable[homeAddress];

            // If there is more than one binding, remainingLifetime will be 0,
            // and no cache entry will be created.
            ushort remainingLifetime = 0;

            IPAddress careOfAddress = this.Client.IPAddress;

            if (entries.Count == 1)
            {
                var entry = entries.First();
                remainingLifetime = (ushort) (entry.ExpirationTime - Simulator.Instance.Clock);
                careOfAddress = entry.CareOfAddress;
            }

            BindingUpdatePacket packet = new BindingUpdatePacket(this.Client, targetAddress);
            packet.Lifetime = remainingLifetime;
            packet.HomeAddress = homeAddress;
            packet.CareOfAddress = careOfAddress;

            return packet;
        }

        /// <summary>
        /// Handles the BeforePacketSent event of the client, in order to
        /// "bypass" the sending of messages destined to some mobile node
        /// currently registered on a foreign agent
        /// </summary>
        /// <param name="source">The node who is going to send the message</param>
        /// <param name="eventArgs">Arguments for the event</param>
        void Client_BeforePacketSent(Node source, Migrator.Events.BeforePacketSentEventArgs eventArgs)
        {
            var packet = eventArgs.Packet;

            // Check for messages destined to my mobile nodes registered
            // with some foreign agent
            if (this.bindingTable.Contains(packet.DestinationIP))
            {
                var entries = this.bindingTable[packet.DestinationIP];

                // For each current binding, send a copy of the packet
                foreach (var entry in entries)
                {
                    // Encapsulate the packet
                    IPPacket outter = new IPPacket(packet.Source, entry.CareOfAddress);
                    outter.Payload = packet;

                    this.Client.Send(outter);

                    // If the route optimization feature is enabled, send a binding
                    // update packet to the sender
                    if (Migrator.Properties.Settings.Default.EnableRouteOptimization)
                    {
                        this.Client.Send(this.InitializeBindingUpdate(entry.HomeAddress, packet.SourceIP));
                    }
                }

                if (entries.Count > 0)
                {
                    // Cancel the event, so the original packet won't be sent
                    eventArgs.Cancel = true;
                }
            }
        }

        /// <summary>
        /// Mobility binding table
        /// </summary>
        private BindingTable bindingTable = new BindingTable();

        private bool supportSimultaneousBindings = true;
        private ushort maxLifetime = 150;
        
        public BindingTable BindingTable
        {
            get { return bindingTable; }
        }
    }
}
